/* @flow */

import type Watcher from './watcher';
import config from '../config';
import { callHook, activateChildComponent } from '../instance/lifecycle';

import { warn, nextTick, devtools, inBrowser, isIE } from '../util/index';

export const MAX_UPDATE_COUNT = 100;

const queue: Array<Watcher> = [];
const activatedChildren: Array<Component> = [];
let has: { [key: number]: ?true } = {};
let circular: { [key: number]: number } = {};
let waiting = false;
let flushing = false;
let index = 0;

/**
 * Reset the scheduler's state.
 */
function resetSchedulerState() {
    index = queue.length = activatedChildren.length = 0;
    has = {};
    if (process.env.NODE_ENV !== 'production') {
        circular = {};
    }
    waiting = flushing = false;
}

// Async edge case #6566 requires saving the timestamp when event listeners are
// attached. However, calling performance.now() has a perf overhead especially
// if the page has thousands of event listeners. Instead, we take a timestamp
// every time the scheduler flushes and use that for all event listeners
// attached during that flush.
export let currentFlushTimestamp = 0;

// Async edge case fix requires storing an event listener's attach timestamp.
let getNow: () => number = Date.now;

// Determine what event timestamp the browser is using. Annoyingly, the
// timestamp can either be hi-res (relative to page load) or low-res
// (relative to UNIX epoch), so in order to compare time we have to use the
// same timestamp type when saving the flush timestamp.
// All IE versions use low-res event timestamps, and have problematic clock
// implementations (#9632)
if (inBrowser && !isIE) {
    const performance = window.performance;
    if (
        performance &&
        typeof performance.now === 'function' &&
        getNow() > document.createEvent('Event').timeStamp
    ) {
        // if the event timestamp, although evaluated AFTER the Date.now(), is
        // smaller than it, it means the event is using a hi-res timestamp,
        // and we need to use the hi-res version for event listener timestamps as
        // well.
        getNow = () => performance.now();
    }
}

/**
 * Flush both queues and run the watchers.
 * 处理队列、watchers
 */
function flushSchedulerQueue() {
    currentFlushTimestamp = getNow();
    flushing = true; //标记当前正在处理watcher队列
    let watcher, id;

    // Sort queue before flush.
    // This ensures that:
    // 1. Components are updated from parent to child. (because parent is always
    //    created before the child)
    // 2. A component's user watchers are run before its render watcher (because
    //    user watchers are created before the render watcher)
    // 3. If a component is destroyed during a parent component's watcher run,
    //    its watchers can be skipped.

    // 1.组件被更新顺序是从父——子组件。因为父组件永远在子组件之前创建
    // 2.组件的用户watcher在对应的渲染watcher之前执行。（因为用户watcher在渲染watcher之前创建的）
    // 3.如果一个组件在它的父组件执行之前被销毁了，那么这个watcher需要被跳过
    // 排序、按照id从小到大。即：安装watcher创建顺序排列
    queue.sort((a, b) => a.id - b.id);

    // do not cache length because more watchers might be pushed
    // as we run existing watchers
    // 不要去缓存这个队列的长度，因为queue在执行过程中可能会往执行队列中添加新的watcher
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        // before 在创建渲染watcher时才有，用于创建 beforeUpdate 钩子函数
        if (watcher.before) {
            watcher.before();
        }
        id = watcher.id;
        has[id] = null; //当数据变化时，下次这个watcher可正常运行
        watcher.run();
        // in dev build, check and stop circular updates.
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
            circular[id] = (circular[id] || 0) + 1;
            if (circular[id] > MAX_UPDATE_COUNT) {
                warn(
                    'You may have an infinite update loop ' +
                        (watcher.user
                            ? `in watcher with expression "${watcher.expression}"`
                            : `in a component render function.`),
                    watcher.vm
                );
                break;
            }
        }
    }

    // keep copies of post queues before resetting state
    const activatedQueue = activatedChildren.slice();
    const updatedQueue = queue.slice();

    resetSchedulerState();

    // call component updated and activated hooks
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);

    // devtool hook
    /* istanbul ignore if */
    if (devtools && config.devtools) {
        devtools.emit('flush');
    }
}

function callUpdatedHooks(queue) {
    let i = queue.length;
    while (i--) {
        const watcher = queue[i];
        const vm = watcher.vm;
        if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'updated');
        }
    }
}

/**
 * Queue a kept-alive component that was activated during patch.
 * The queue will be processed after the entire tree has been patched.
 */
export function queueActivatedComponent(vm: Component) {
    // setting _inactive to false here so that a render function can
    // rely on checking whether it's in an inactive tree (e.g. router-view)
    vm._inactive = false;
    activatedChildren.push(vm);
}

function callActivatedHooks(queue) {
    for (let i = 0; i < queue.length; i++) {
        queue[i]._inactive = true;
        activateChildComponent(queue[i], true /* true */);
    }
}

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 *
 * 把当前watcher放入一个队列
 */
export function queueWatcher(watcher: Watcher) {
    const id = watcher.id;
    // has 是对象，通过id获取has中的值。null标识watcher还未处理。防止watcher重复处理。
    if (has[id] == null) {
        has[id] = true;
        // flushing 正在刷新。true：queue 队列正在处理，queue队列中都为watcher对象，即watcher对象正在被处理。
        if (!flushing) {
            // 当前队列没在处理时，直接将watcher放入队列末尾
            queue.push(watcher);
        } else {
            // if already flushing, splice the watcher based on its id
            // if already past its id, it will be run next immediately.
            // 如果 queue 队列正在处理，给当前watcher找到一个合适的位置
            // 如果已经在处理，则根据其id拼接watcher
            // 如果已经超过了它的id，它将立即下一个运行
            let i = queue.length - 1; //获取当前队列长度
            // index 当前队列正在处理的位置。如果正在处理的id大于目前watcher.id，则把watcher放入该位置
            while (i > index && queue[i].id > watcher.id) {
                i--;
            }
            queue.splice(i + 1, 0, watcher);
        }
        // queue the flush
        // 判断当前queue队列是否在被执行。
        if (!waiting) {
            // 标记正在执行
            waiting = true;

            if (process.env.NODE_ENV !== 'production' && !config.async) {
                flushSchedulerQueue();
                return;
            }

            nextTick(flushSchedulerQueue);
        }
    }
}
